/* QBE.m:
 * You may freely copy, distribute, and reuse the code in this example.
 * NeXT disclaims any warranty of any kind, expressed or  implied, as to its
 * fitness for any particular use.
 *
 * Written by Craig Federighi
 *  
 *
 * Example of a Query By Example object: Connects to an EOController for an
 * easy contruction of qualifiers from data entered into fields of an 
 * existing user interface.
 */
 
#import "QBE.h"
#import "DictionaryDataSource.h"

#import <eoaccess/eoaccess.h>
#import <foundation/NSString.h>
#import <foundation/NSArray.h>

#import <foundation/NSCharacterSet.h>
#import <foundation/NSScanner.h>



/* A private category to strip white spaces */
 
@interface NSString(_QBEStripWhite) 
- (NSString *)stringStrippingLeadingCharacterSet:(NSCharacterSet *)skipset;
@end

@implementation NSString(_QBEStripWhite) 

/* Returns autoreleased string produced by generating new string
 * with the leading junk skipped.
 */
- (NSString *)stringStrippingLeadingCharacterSet:(NSCharacterSet *)skipset
  {
    NSRange range;
    
    range = [self rangeOfCharacterFromSet: [skipset invertedSet]];
    return [self substringFromIndex: range.location];
}

@end

/*  A category on EOAssociation to turn on and off editability of
 *  underlying UI objects
 */
@interface EOAssociation(_QBEEditable)
- (BOOL)isEditable;
- (void)setEditable:(BOOL)yn;
@end

@implementation EOAssociation(_QBEEditable)
- (BOOL)isEditable { return NO; }
- (void)setEditable:(BOOL)yn {}
@end

@implementation EOControlAssociation(_QBEEditable)
- (BOOL)isEditable {
    Cell * cell;
    if ([[self destination] isKindOfClass:[ActionCell class]])
        cell = [self destination];
    else 
        cell = [[self destination] cell];
	
    return [cell isEditable]; 
}

- (void)setEditable:(BOOL)yn {
    Cell * cell;
    if ([[self destination] isKindOfClass:[ActionCell class]])
        cell = [self destination];
    else 
        cell = [[self destination] cell];

    [cell setEditable:yn]; 
}
@end

@implementation EOColumnAssociation(_QBEEditable)
- (BOOL)isEditable {return [tableView isEditable]; }
- (void)setEditable:(BOOL)yn 
{ 
    [tableView setEditable:yn]; 
}
@end


@implementation EOActionCellAssociation(_QBEEditable)
- (BOOL)isEditable { 
    return [(Cell *)[self destination] isEditable]; 
}

- (void)setEditable:(BOOL)yn {
    [(Cell *)[self destination] setEditable:yn]; 
}
@end


/* Need to tell what sort of associations can participate in QBE
 * editing.  Master-detail associations, for example, should be 
 * deactivated.
 */
@interface EOAssociation(_isQBEEditor)
- (BOOL)isQBEEditor; // Should remain active in QBE editing
@end

@implementation EOAssociation(_isQBEEditor)
- (BOOL)isQBEEditor  {return YES;}
@end

@implementation EOQualifiedAssociation(_isQBEEditor)
- (BOOL)isQBEEditor  {return NO;}
@end

// Make sure an Adaptor is ready to convert values
// (the SybaseAdaptor can't do this until after it's connected)
@interface EOAdaptorChannel (canConvert)
- (BOOL)canConvertValues;
@end

@implementation EOAdaptorChannel (canConvert)
- (BOOL)canConvertValues
{
    EOAdaptorChannel *adaptorChannel = nil;
    EOAdaptor *adaptor;
    BOOL retValue = YES;

    if (![self isOpen]) {
        adaptor = [[adaptorChannel adaptorContext] adaptor];
        if (![adaptor hasValidConnectionDictionary]){
            if (![adaptor runLoginPanelAndValidateConnectionDictionary])
                retValue = NO;
        }

        if (retValue == YES && ![self openChannel])
            retValue = NO;
    }
    return retValue;
}
@end



/* Actual implementation of QBE object */

@implementation QBE

- init
{
    return self;
}

static NSString *operators[] = {
    @"=",
    @">",
    @"<",
    @"<=",
    @">=",
    NULL
};


/* Build the qualifier based on the user input */

- qualifierForKey:(NSString *)key value:value entity:(EOEntity *)entity
{
    EOAdaptorChannel *adaptorChannel = [[realsource databaseChannel] adaptorChannel];
    EOAdaptor *adaptor = [[adaptorChannel adaptorContext] adaptor];
    NSString **ops;
    NSString *op = nil, *fmt;
    id target = value;
    EOQualifier *qualifier;
    EOAttribute *attr;
    BOOL isStringAttribute;
    
    // Be sure that we can connect to the database before constructing a
    // qualifier.  Without a connection, the adaptor can't
    // formatValue:forAttribute:
    if (![adaptorChannel canConvertValues])
        return nil;
    
    [adaptorChannel setDebugEnabled:YES];
    
    // Check type of attribute so we know whether to quote target
    attr = [entity attributeNamed:key];
    isStringAttribute = (strcmp([attr valueClassName], "NSString") == 0);
    
    // Look for an operator.  If we see one, rip it off and use it    
    if ([value isKindOfClass:[NSString class]]) {
        for(ops=operators; *ops; ops++) {
	    if ( [(NSString *)value hasPrefix: *ops] ) {
		op = *ops;
		
		// Need to strip whitespace!
		target = [[(NSString *)value substringFromIndex: [op length]]
				stringStrippingLeadingCharacterSet: 
			           [NSCharacterSet whitespaceCharacterSet]];
		break;
	    }
	}
    }

    // compute format string
    if (isStringAttribute && !op) {
	// Wildcard search
	op = @"LIKE";
	target = [NSString stringWithFormat:@"%%%@%%", target];
    } else if (!op) {
	// Default for non string attributes
	op = @"=";
    }

    fmt = @"%A %@ %@";
    qualifier = [[EOQualifier alloc] initWithEntity:entity 
	    qualifierFormat:fmt, key, op,
	    [adaptor formatValue:target forAttribute:attr]];
    return [qualifier autorelease];
}


- (EOQualifier *)makeQualifierForEo:eo entity:(EOEntity *)entity 
				    conjoin:(BOOL)isConjoin
  // Construct a qualifier for an EO
{
    NSDictionary *valueDict;
    NSArray *attributes;
    NSMutableArray *attrnames;
    int count, i;
    EOQualifier *qualifier, *root = nil;
    
    // get a dictionary of values for the eo
    attributes = [entity attributes];

    count = [attributes count];
    attrnames = [NSMutableArray arrayWithCapacity:count];
    
    // We should really only ask for the classAttributes
    for(i=0; i<count; i++)
    	[attrnames addObject:
	    [(EOAttribute *)[attributes objectAtIndex:i] name]];

    valueDict = [eo valuesForKeys:attrnames];   

    for(i=0; i<count; i++) {
        NSString *key = [attrnames objectAtIndex:i];
	id value = [valueDict objectForKey:key];
	if( value ) {
	    qualifier = [self qualifierForKey:key value:value entity:entity];
	    if (qualifier) {
	        if (root) {
		    if (isConjoin)
		    	[root conjoinWithQualifier:qualifier];
		    else
		    	[root disjoinWithQualifier:qualifier];
		} else 
		    root = qualifier;
	    }
	}			
    }
    
    return root;
}

- makeQualifier:(BOOL)isConjoin
{
    EOEntity *entity;
    NSArray *objects;
    EOQualifier *root = nil;
    int count, i;
    
    // force flush of changes to objects
    [controller saveToObjects];
    objects = [controller allObjects];    
    entity = [realsource entity];

    for(count=[objects count],i=0; i < count; i++) {
        id eo = [objects objectAtIndex:i];
	EOQualifier *q = [self makeQualifierForEo:eo entity:entity
				    conjoin:isConjoin];
        if (root && q)
	    [root disjoinWithQualifier:q];
	else 
	    root = q;
    } 

    return root;
}

- enterQueryMode:sender
// tweak the controller into QBE state:
//  - no records, buffer edits, save to objects
{
    DictionaryDataSource *dictsource;
    NSArray *associations;
    int i, count;
    EOEntity *entity;
    
    if (realsource) 
        return [self addQuery:nil];
    
    // flush everything we have now
    [controller saveToDataSource];

    // swap in tempory datasource
    realsource = [(EODatabaseDataSource *)[controller dataSource] retain];
    dictsource = [[[DictionaryDataSource alloc] init] autorelease];
    [controller setDataSource: dictsource];
    
    // We don't want any detail controllers to be updated to match the
    // QBE entry (since it's meaningless and probably not complete)
    
    // Prepare array to add associations to
    removedAssociations = [[NSMutableArray alloc] init];
    
    // Remove all Qualified Associations
    associations = [controller associations];
    for(count=[associations count], i=0; i < count; i++) {
        EOAssociation *assoc = [associations objectAtIndex: i];
	if (![assoc isQBEEditor]) {
	    [removedAssociations addObject: assoc];
	    [controller removeAssociation:assoc];
	}
    }
    
    // Remove the next controller (for a fetch)
    nextController = [controller nextController];
    [controller setNextController: nil];
    
    // unset the delegate
    controllersDelegate = [controller delegate];
    [controller setDelegate:nil];

    // Should force all "qualifyable" associations (to database attributes) 
    // to editable and all others uneditable.

    // Make all remaining associations editable
    // Remember those that weren't so we can restore them.
    entity = [realsource entity];
    wereUneditableAssoc = [[NSMutableArray alloc] init];
    wereEditableAssoc = [[NSMutableArray alloc] init];
    associations = [controller associations];

    // Note which are uneditable
    for(count=[associations count], i=0; i < count; i++) {
        EOAssociation *assoc = [associations objectAtIndex: i];
	BOOL isQualifyable = ([entity attributeNamed:[assoc key]] != nil);
	if ( isQualifyable && ![assoc isEditable] ) {
	    [wereUneditableAssoc addObject:assoc];
	} else if (!isQualifyable && [assoc isEditable]) {
	    [wereUneditableAssoc addObject:assoc];
	}
    }	
    // make them temporily editable
    for(count=[wereUneditableAssoc count], i=0; i < count; i++) {
        EOAssociation *assoc = [wereUneditableAssoc objectAtIndex: i];
	[assoc setEditable:YES];
    }	
    // make them temporily uneditable
    for(count=[wereEditableAssoc count], i=0; i < count; i++) {
        EOAssociation *assoc = [wereEditableAssoc objectAtIndex: i];
	[assoc setEditable:NO];
    }	

    return [self addQuery:nil];
}

- addQuery:sender
{
    int count;
    if (!realsource) return nil;

    count = [[controller allObjects] count];
    
    [controller insertObjectAtIndex:count];
    [controller setSelectionIndexes:
	    [NSArray arrayWithObject:[NSNumber numberWithInt:count]]];

    return self;
}

- exitQueryMode:sender
    // restore old datasource and controller settings
{
    int count, i;
    
    // restore datasource
    [controller setDataSource: realsource]; 
    realsource = nil; // mark that we're out of query mode
    
    // restore removed qualified associations
    for(count=[removedAssociations count], i=0; i<count; i++) {
        EOAssociation *assoc = [removedAssociations objectAtIndex:i];
        [controller addAssociation:assoc];
    }
    [removedAssociations release];

    // Reset uneditable associations to back to uneditable again
    for(count=[wereUneditableAssoc count], i=0; i<count; i++) {
        EOAssociation *assoc = [wereUneditableAssoc objectAtIndex:i];
        [assoc setEditable:NO];
    }
    [wereUneditableAssoc release];
    
    // Reset editable associations to back to editable again
    for(count=[wereEditableAssoc count], i=0; i<count; i++) {
        EOAssociation *assoc = [wereEditableAssoc objectAtIndex:i];
        [assoc setEditable:YES];
    }
    [wereEditableAssoc release];
    
    
    // restore next controller
    [controller setNextController:nextController];
    
    // restore the delegate
    [controller setDelegate:controllersDelegate];
    
    [controller fetch];    

    return self;
}

- actionOutsideQueryMode
{
    // We're not in query mode and they pushed a query button.
    // Reset the qualifier and do a basic fetch
    EODatabaseDataSource *source = (EODatabaseDataSource *)[controller dataSource];
    [source setAuxiliaryQualifier:nil];
    [controller fetch];
    return self;
}

- applyQualifier:sender
// set qualifier to AND query for current attr settings, 
// exit query mode and fetch
{
    if (!realsource) 
    	return [self actionOutsideQueryMode];
    
    [realsource setAuxiliaryQualifier:[self makeQualifier:YES]];  // conjoin
    return [self exitQueryMode:self];
}

- applyDisjointQualifier:sender
// set qualifier to AND query for current attr settings, 
// exit query mode and fetch
{
    if (!realsource) 
    	return [self actionOutsideQueryMode];
    
    [realsource setAuxiliaryQualifier:[self makeQualifier:NO]];  // Disjoin
    return [self exitQueryMode:self];
}

- toggleQueryFetch:sender
{
    return (realsource) ? [self applyQualifier:sender] 
                            : [self enterQueryMode:sender];
}

@end

